#!/bin/sh

#  git-remote-gcrypt
#
#  Copyright (c) 2013 engla
#  Copyright (c) 2013, 2014 Joey Hess <id@joeyh.name>
#  Copyright (c) 2016, 2018 Sean Whitton <spwhitton@spwhitton.name> and contributors
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) version 2 or any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#  See README.rst for usage instructions

set -e # errexit
set -f # noglob
set -C # noclobber

export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked
Gref="refs/gcrypt/gitception$GITCEPTION"
Gref_rbranch="refs/heads/master"
Packkey_bytes=63  # nbr random bytes for packfile keys, any >= 256 bit is ok
Hashtype=SHA256   # SHA512 SHA384 SHA256 SHA224 supported.
Manifestfile=91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a
Hex40="[a-f0-9]"
Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40
Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40 # Match SHA-1 hexdigest
GPG="$(git config --get "gpg.program" '.+' || echo gpg)"

Did_find_repo=  # yes for connected, no for no repo
Localdir="${GIT_DIR:=.git}/remote-gcrypt"
Tempdir=

Repoid=
Refslist=
Packlist=
Keeplist=
Extnlist=
Repack_limit=25

Recipients=

# compat/utility functions
# xfeed: The most basic output function puts $1 into the stdin of $2..$#
xfeed()
{
	local input_=
	input_=$1; shift
	"$@" <<EOF
$input_
EOF
}
xecho() { xfeed "$*" cat; }
xecho_n() { xecho "$@" | tr -d \\n ; } # kill newlines
echo_git() { xecho "$@" ; }  # Code clarity
echo_info() { xecho "gcrypt:" "$@" >&2; }
echo_die() { echo_info "$@" ; exit 1; }

isnull() { case "$1" in "") return 0;; *) return 1;; esac; }
isnonnull() { ! isnull "$1"; }
iseq() { case "$1" in "$2") return 0;; *) return 1;; esac; }
isnoteq() { ! iseq "$1" "$2"; }
negate() { ! "$@"; }

# Execute $@ or die
pipefail()
{
	"$@" || { echo_info "'$1' failed!"; kill $$; exit 1; }
}

isurl() { isnull "${2%%$1://*}"; }
islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; }

xgrep() { command grep "$@" || : ; }

# setvar is used for named return variables
# $1 *must* be a valid variable name, $2 is any value
#
# Conventions
#   return variable names are passed with a @ prefix
#   return variable functions use f_ prefix local vars
#   return var consumers use r_ prefix vars (or Titlecase globals)
setvar()
{
	isnull "${1##@*}" || echo_die "Missing @ for return variable: $1"
	eval ${1#@}=\$2
}

Newline="
"

# $1 is return var, $2 is value appended with newline separator
append_to()
{
	local f_append_tmp_=
	eval f_append_tmp_=\$${1#@}
	isnull "$f_append_tmp_" || f_append_tmp_=$f_append_tmp_$Newline
	setvar "$1" "$f_append_tmp_$2"
}

# Pick words from each line
# $1 return variable name
# $2 input value
pick_fields_1_2()
{
	local f_ret= f_one= f_two=
	while read f_one f_two _ # from << here-document
	do
		f_ret="$f_ret$f_one $f_two$Newline"
	done <<EOF
$2
EOF
	setvar "$1" "${f_ret#$Newline}"
}

# Take all lines matching $2 (full line)
# $1 return variable name
# $2 filter word
# $3 input value
#  if $1 is a literal `!', the match is reversed (and arguments shift)
#  we instead remove all lines matching
filter_to()
{
	local f_neg= f_line= f_ret= IFS=
	isnoteq "$1" "!" || { f_neg=negate; shift; }
	IFS=$Newline
	for f_line in $3
	do
		$f_neg isnonnull "${f_line##$2}" || f_ret=$f_ret$f_line$Newline
	done
	setvar "$1" "${f_ret%$Newline}"
}

# Output the number of lines in $1
line_count()
{
	local IFS=
	IFS=$Newline
	set -- $1
	xecho "$#"
}

# Convert URI in standard or nonstandard form to rsync's user@host:path
rsynclocation ()
{
	echo "${1#rsync://}" | sed 's/\(^[^:/]*\)\//\1:\//'
}


## gitception part
# Fetch giturl $1, file $2
gitception_get()
{
	# Take care to preserve FETCH_HEAD
	local ret_=: obj_id= fet_head="$GIT_DIR/FETCH_HEAD"
	[ -e "$fet_head" ] && command mv -f "$fet_head" "$fet_head.$$~" || :
	git fetch -q -f "$1" "$Gref_rbranch:$Gref" >/dev/null &&
		obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
		isnonnull "$obj_id" && git cat-file blob "$obj_id" && ret_=: ||
		{ ret_=false && : ; }
	[ -e "$fet_head.$$~" ] && command mv -f "$fet_head.$$~" "$fet_head" || :
	$ret_
}

anon_commit()
{
	GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \
	GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \
	GIT_COMMITTER_EMAIL="root@localhost" \
	GIT_COMMITTER_DATE="1356994801 -0400" \
		git commit-tree "$@" <<EOF
Initial commit
EOF
}

# Get 'tree' from $1, change file $2 to obj id $3
update_tree()
{
	local tab_="	"
	# $2 is a filename from the repo format
	(set +e;
		git ls-tree "$1" | xgrep -v -E '\b'"$2"'$';
		xecho "100644 blob $3$tab_$2"
	) | git mktree
}

# Put giturl $1, file $2
# depends on previous GET to set $Gref and depends on PUT_FINAL later
gitception_put()
{
	local obj_id= tree_id= commit_id=
	obj_id=$(git hash-object -w --stdin) &&
		tree_id=$(update_tree "$Gref" "$2" "$obj_id") &&
		commit_id=$(anon_commit "$tree_id") &&
		git update-ref "$Gref" "$commit_id"
}

# Remove giturl $1, file $2
# depends on previous GET like put
gitception_remove()
{
	local tree_id= commit_id= tab_="	"
	# $2 is a filename from the repo format
	tree_id=$(git ls-tree "$Gref" | xgrep -v -E '\b'"$2"'$' | git mktree) &&
		commit_id=$(anon_commit "$tree_id") &&
		git update-ref "$Gref" "$commit_id"
}

gitception_new_repo()
{
	local commit_id= empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
	# get any file to update Gref, and if it's not updated we create empty
	git update-ref -d "$Gref" || :
	gitception_get "$1" "x" 2>/dev/null >&2 || :
	git rev-parse -q --verify "$Gref" >/dev/null && return 0 ||
		commit_id=$(anon_commit "$empty_tree") &&
		git update-ref "$Gref" "$commit_id"
}
## end gitception

# Fetch repo $1, file $2, tmpfile in $3
GET()
{
	if isurl sftp "$1"
	then
		(exec 0>&-; curl -s -S -k "$1/$2") > "$3"
	elif isurl rsync "$1"
	then
		(exec 0>&-; rsync -I -W "$(rsynclocation "$1")"/"$2" "$3" >&2)
	elif isurl rclone "$1"
	then
		(exec 0>&-; rclone copyto --error-on-no-transfer "${1#rclone://}"/"$2" "$3" >&2)
	elif islocalrepo "$1"
	then
		cat "$1/$2" > "$3"
	else
		gitception_get "${1#gitception://}" "$2" > "$3"
	fi
}

# Put repo $1, file $2 or fail, tmpfile in $3
PUT()
{
	if isurl sftp "$1"
	then
		curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2"
	elif isurl rsync "$1"
	then
		rsync $Conf_rsync_put_flags -I -W "$3" "$(rsynclocation "$1")"/"$2" >&2
	elif isurl rclone "$1"
	then
		rclone copyto --error-on-no-transfer "$3" "${1#rclone://}"/"$2" >&2
	elif islocalrepo "$1"
	then
		cat >| "$1/$2" < "$3"
	else
		gitception_put "${1#gitception://}" "$2" < "$3"
	fi
}

# Put all PUT changes for repo $1 at once
PUT_FINAL()
{
	if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1" || isurl rclone "$1"
	then
		:
	else
		git push --quiet -f "${1#gitception://}" "$Gref:$Gref_rbranch"
	fi
}

# Put directory for repo $1
PUTREPO()
{
	if isurl sftp "$1"
	then
		:
	elif isurl rsync "$1"
	then
		rsync $Conf_rsync_put_flags -q -r --exclude='*' \
			"$Localdir/" "$(rsynclocation "$1")" >&2
	elif isurl rclone "$1"
	then
		rclone mkdir "${1#rclone://}" >&2
	elif islocalrepo "$1"
	then
		mkdir -p "$1"
	else
		gitception_new_repo "${1#gitception://}"
	fi
}

# For repo $1, delete all newline-separated files in $2
REMOVE()
{
	local fn_=
	if isurl sftp "$1"
	then
		# FIXME
		echo_info "sftp: Ignore remove request $1/$2"
	elif isurl rsync "$1"
	then
		xfeed "$2" rsync -I -W -v -r --delete --include-from=- \
			--exclude='*' "$Localdir"/ "$(rsynclocation "$1")/" >&2
	elif isurl rclone "$1"
	then
		xfeed "$2" rclone delete -v --include-from=/dev/stdin "${1#rclone://}/" >&2
	elif islocalrepo "$1"
	then
		for fn_ in $2; do
			rm -f "$1"/"$fn_"
		done
	else
		for fn_ in $2; do
			gitception_remove "${1#gitception://}" "$fn_"
		done
	fi
}

CLEAN_FINAL()
{
	if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1" || isurl rclone "$1"
	then
		:
	else
		git update-ref -d "$Gref" || :
	fi
}

ENCRYPT()
{
	rungpg --batch --force-mdc --compress-algo none --trust-model=always --passphrase-fd 3 -c 3<<EOF
$1
EOF
}

DECRYPT()
{
	rungpg -q --batch --no-default-keyring --secret-keyring /dev/null \
		--keyring /dev/null --passphrase-fd 3 -d  3<<EOF
$1
EOF
}

# Encrypt to recipients $1
PRIVENCRYPT()
{
	set -- $1
	if isnonnull "$Conf_signkey"; then
		set -- "$@" -u "$Conf_signkey"
	fi
	rungpg --compress-algo none --trust-model=always -se "$@"
}

# $1 is the match for good signature, $2 is the textual signers list
PRIVDECRYPT()
{
	local status_=
	exec 4>&1 &&
	status_=$(rungpg --status-fd 3 -q -d 3>&1 1>&4) &&
	xfeed "$status_" grep "^\[GNUPG:\] ENC_TO " >/dev/null &&
	(xfeed "$status_" grep -e "$1" >/dev/null || {
		echo_info "Failed to verify manifest signature!" &&
		echo_info "Only accepting signatories: ${2:-(none)}" &&
		return 1
	})
}

# Generate $1 random bytes
genkey()
{
	rungpg --armor --gen-rand 1 "$1"
}

gpg_hash()
{
	local hash_=
	hash_=$(rungpg --with-colons --print-md "$1" | tr A-F a-f)
	hash_=${hash_#:*:}
	xecho "${hash_%:}"
}

rungpg()
{
	if isnonnull "$Conf_gpg_args"; then
		set -- "$Conf_gpg_args" "$@"
	fi
	# gpg will fail to run when there is no controlling tty,
	# due to trying to print messages to it, even if a gpg agent is set
	# up. --no-tty fixes this.
	if [ "x$GPG_AGENT_INFO" != "x" ]; then
		${GPG} --no-tty $@
	else
		${GPG} $@
	fi
}

# Pass the branch/ref by pipe to git
safe_git_rev_parse()
{
	git cat-file --batch-check 2>/dev/null |
		xgrep -v "missing" | cut -f 1 -d ' '
}

make_new_repo()
{
	echo_info "Setting up new repository"
	PUTREPO "$URL"

	# Needed assumption: the same user should have no duplicate Repoid
	Repoid=":id:$(genkey 15)"
	iseq "${NAME#gcrypt::}" "$URL" ||
		git config "remote.$NAME.gcrypt-id" "$Repoid"
	echo_info "Remote ID is $Repoid"
	Extnlist="extn comment"
}


# $1 return var for goodsig match, $2 return var for signers text
read_config()
{
	local recp_= r_tail= r_keyinfo= r_keyfpr= gpg_list= cap_= conf_part= good_sig= signers_=
	Conf_signkey=$(git config --get "remote.$NAME.gcrypt-signingkey" '.+' ||
		git config --path user.signingkey || :)
	conf_part=$(git config --get "remote.$NAME.gcrypt-participants" '.+' ||
		git config --get gcrypt.participants '.+' || :)
	Conf_pubish_participants=$(git config --get --bool "remote.$NAME.gcrypt-publish-participants" '.+' ||
		git config --get --bool gcrypt.publish-participants || :)
	Conf_gpg_args=$(git config --get gcrypt.gpg-args '.+' || :)
	Conf_rsync_put_flags=$(git config --get "remote.$NAME.gcrypt-rsync-put-flags" '.+' ||
		git config --get "gcrypt.rsync-put-flags" '.+' || :)
	Conf_force_required=$(git config --get --bool "remote.$NAME.gcrypt-require-explicit-force-push" '.+' ||
		git config --get --bool gcrypt.require-explicit-force-push '.+' || :)

	# Figure out which keys we should encrypt to or accept signatures from
	if isnull "$conf_part" || iseq "$conf_part" simple
	then
		signers_="(default keyring)"
		Recipients="--throw-keyids --default-recipient-self"
		good_sig="^\[GNUPG:\] GOODSIG "
		setvar "$1" "$good_sig"
		setvar "$2" "$signers_"
		return 0
	fi

	for recp_ in $conf_part
	do
		gpg_list=$(rungpg --with-colons --fingerprint -k "$recp_")
		r_tail_=$(echo "$recp_" | sed -e 's/^0x//')
		filter_to @r_keyinfo "pub*" "$gpg_list"
		if echo "$recp_" | grep -E -q '^[xA-F0-9]+$'; then # is $recp_ a keyid?
			filter_to @r_keyfpr "fpr*$r_tail_*" "$gpg_list"
		else
			filter_to @r_keyfpr "fpr*" "$gpg_list"
		fi
		isnull "$r_keyinfo" || isnonnull "${r_keyinfo##*"$Newline"*}" ||
		echo_info "WARNING: '$recp_' matches multiple keys, using one"
		isnull "$r_keyfpr" || isnonnull "${r_keyfpr##*"$Newline"*}" ||
		echo_info "WARNING: '$recp_' matches multiple fingerprints, using one"
		r_keyinfo=${r_keyinfo%%"$Newline"*}
		r_keyfpr=${r_keyfpr%%"$Newline"*}
		keyid_=$(xfeed "$r_keyinfo" cut -f 5 -d :)
		fprid_=$(xfeed "$r_keyfpr" cut -f 10 -d :)

		isnonnull "$fprid_" &&
		signers_="$signers_ $keyid_" &&
		append_to @good_sig "^\[GNUPG:\] VALIDSIG .*$fprid_$" || {
			echo_info "WARNING: Skipping missing key $recp_"
			continue
		}
		# Check 'E'ncrypt capability
		cap_=$(xfeed "$r_keyinfo" cut -f 12 -d :)
		if ! iseq "${cap_#*E}" "$cap_"; then
			if [ "$Conf_pubish_participants" = true ]; then
				Recipients="$Recipients -r $keyid_"
			else
				Recipients="$Recipients -R $keyid_"
			fi
		fi
	done

	if isnull "$Recipients"
	then
		echo_info "You have not configured any keys you can encrypt to" \
			"for this repository"
		echo_info "Use ::"
		echo_info "  git config gcrypt.participants YOURKEYID"
		exit 1
	fi
	setvar "$1" "$good_sig"
	setvar "$2" "$signers_"
}

ensure_connected()
{
	local manifest_= r_repoid= r_name= url_frag= r_sigmatch= r_signers= \
		tmp_manifest= tmp_stderr=

	if isnonnull "$Did_find_repo"
	then
		return
	fi
	Did_find_repo=no
	read_config @r_sigmatch @r_signers

	iseq "${NAME#gcrypt::}" "$URL" || r_name=$NAME

	if isurl gitception "$URL" && isnonnull "$r_name"; then
		git config "remote.$r_name.url" "gcrypt::${URL#gitception://}"
		echo_info "Updated URL for $r_name, gitception:// -> ()"
	fi

	# Find the URL fragment
	url_frag=${URL##*"#"}
	isnoteq "$url_frag" "$URL" || url_frag=
	URL=${URL%"#$url_frag"}

	# manifestfile -- sha224 hash if we can, else the default location
	if isurl sftp "$URL" || islocalrepo "$URL" || isurl rsync "$URL" || isurl rclone "$URL"
	then
		# not for gitception
		isnull "$url_frag" ||
			Manifestfile=$(xecho_n "$url_frag" | gpg_hash SHA224)
	else
		isnull "$url_frag" || Gref_rbranch="refs/heads/$url_frag"
	fi

	Repoid=
	isnull "$r_name" ||
		Repoid=$(git config "remote.$r_name.gcrypt-id" || :)


	tmp_manifest="$Tempdir/maniF"
	tmp_stderr="$Tempdir/stderr"
	GET "$URL" "$Manifestfile" "$tmp_manifest" 2>| "$tmp_stderr" || {
		if ! isnull "$Repoid"; then
			cat >&2 "$tmp_stderr"
			echo_info "Repository not found: $URL"
			echo_info "..but repository ID is set. Aborting."
			return 1
		else
			echo_info "Repository not found: $URL"
			return 0
		fi
	}

	Did_find_repo=yes
	echo_info "Decrypting manifest"
	manifest_=$(PRIVDECRYPT "$r_sigmatch" "$r_signers" < "$tmp_manifest") &&
		isnonnull "$manifest_" ||
		echo_die "Failed to decrypt manifest!"
	rm -f "$tmp_manifest"

	filter_to @Refslist "$Hex40 *" "$manifest_"
	filter_to @Packlist "pack :*:* *" "$manifest_"
	filter_to @Keeplist "keep :*:*" "$manifest_"
	filter_to @Extnlist "extn *" "$manifest_"
	filter_to @r_repoid "repo *" "$manifest_"

	r_repoid=${r_repoid#repo }
	r_repoid=${r_repoid% *}
	if isnull "$Repoid"
	then
		echo_info "Remote ID is $r_repoid"
		Repoid=$r_repoid
	elif isnoteq "$r_repoid" "$Repoid"
	then
		echo_info "WARNING:"
		echo_info "WARNING: Remote ID has changed!"
		echo_info "WARNING: from $Repoid"
		echo_info "WARNING: to   $r_repoid"
		echo_info "WARNING:"
		Repoid=$r_repoid
	else
		return 0
	fi

	isnull "$r_name" || git config "remote.$r_name.gcrypt-id" "$r_repoid"
}

# $1 is the hash type (SHA256 etc)
# $2 the pack id
# $3 the key
get_verify_decrypt_pack()
{
	local rcv_id= tmp_encrypted=
	tmp_encrypted="$Tempdir/packF"
	GET "$URL" "$2" "$tmp_encrypted" &&
	rcv_id=$(gpg_hash "$1" < "$tmp_encrypted") &&
	iseq "$rcv_id" "$2" || echo_die "Packfile $2 does not match digest!"
	DECRYPT "$3" < "$tmp_encrypted"
	rm -f "$tmp_encrypted"
}

# download all packlines (pack :SHA256:a32abc1231) from stdin (or die)
# $1 destdir (when repack, else "")
get_pack_files()
{
	local pack_id= r_pack_key_line= htype_= pack_= key_=
	while IFS=': ' read -r _ htype_ pack_ # <<here-document
	do
		isnonnull "$pack_" || continue

		# Get the Packlist line with the key
		pack_id=":${htype_}:$pack_"
		filter_to @r_pack_key_line "pack $pack_id *" "$Packlist"
		key_=${r_pack_key_line#pack $pack_id }

		if isnonnull "${pack_##$Hex40*}" ||
			isnoteq "$htype_" SHA256 && isnoteq "$htype_" SHA224 &&
			isnoteq "$htype_" SHA384 && isnoteq "$htype_" SHA512
		then
			echo_die "Packline malformed: $pack_id"
		fi

		get_verify_decrypt_pack "$htype_" "$pack_" "$key_" | \
		if isnull "${1:-}"
		then
			# add to local pack list
			git index-pack -v --stdin >/dev/null
			xecho "pack $pack_id" >> "$Localdir/have_packs$GITCEPTION"
		else
			git index-pack -v --stdin "$1/${pack_}.pack" >/dev/null
		fi
	done
}

# Download and unpack remote packfiles
# $1 return var for list of packfiles to delete
repack_if_needed()
{
	local n_= m_= kline_= r_line= r_keep_packlist= r_del_list=

	isnonnull "$Packlist" || return 0

	if isnonnull "${GCRYPT_FULL_REPACK:-}"
	then
		Keeplist=
		Repack_limit=0
	fi

	pick_fields_1_2 @r_del_list "$Packlist"

	n_=$(line_count "$Packlist")
	m_=$(line_count "$Keeplist")
	if iseq 0 "$(( $Repack_limit < ($n_ - $m_) ))"; then
		return
	fi
	echo_info "Repacking remote $NAME, ..."

	mkdir "$Tempdir/pack"

	# Split packages to keep and to repack
	if isnonnull "$Keeplist"; then
		while read -r _ kline_ _ # <<here-document
		do
			isnonnull "$kline_" || continue
			filter_to @r_line "pack $kline_ *" "$Packlist"
			append_to @r_keep_packlist "$r_line"
			filter_to ! @r_del_list "pack $kline_" "$r_del_list"
		done <<EOF
$Keeplist
EOF
	fi

	xfeed "$r_del_list" get_pack_files "$Tempdir/pack/"

	(set +f; pipefail git verify-pack -v "$Tempdir"/pack/*.idx) |
		grep -E '^[0-9a-f]{40}' | cut -f 1 -d ' '

	Packlist=$r_keep_packlist
	setvar "$1" "$r_del_list"
}

do_capabilities()
{
	echo_git fetch
	echo_git push
	echo_git
}

do_list()
{
	local obj_id= ref_name= line_=
	ensure_connected

	xecho "$Refslist" | while read line_
	do
		isnonnull "$line_" || break
		obj_id=${line_%% *}
		ref_name=${line_##* }
		echo_git "$obj_id" "$ref_name"
		if iseq "$ref_name" "refs/heads/master"
		then
			echo_git "@refs/heads/master HEAD"
		fi
	done

	# end with blank line
	echo_git
}

do_fetch()
{
	# Download packs in the manifest that don't appear in have_packs
	local pneed_= premote_=

	ensure_connected

	# The `+` for $GITCEPTION is pointless but we will be safe for stacking
	pick_fields_1_2 @premote_ "$Packlist"
	if [ -s "$Localdir/have_packs+" ]
	then
		pneed_=$(xfeed "$premote_" xgrep -v -x -f "$Localdir/have_packs+")
	else
		pneed_=$premote_
	fi

	xfeed "$pneed_" get_pack_files

	echo_git # end with blank line
}

# do_push PUSHARGS (multiple lines like +src:dst, with both + and src opt.)
do_push()
{
	# Security protocol:
	# Each git packfile is encrypted and then named for the encrypted
	# file's hash. The manifest is updated with the pack id.
	# The manifest is encrypted.
	local r_revlist= pack_id= key_= obj_= src_= dst_= \
		r_pack_delete= tmp_encrypted= tmp_objlist= tmp_manifest= \
		force_passed=

	ensure_connected

	if iseq "$Did_find_repo" "no"
	then
		make_new_repo
	fi

	if isnonnull "$Refslist"
	then
		# mark all remote refs with  ^<sha-1> (if sha-1 exists locally)
		r_revlist=$(xfeed "$Refslist" cut -f 1 -d ' ' |
			safe_git_rev_parse | sed -e 's/^\(.\)/^&/')
	fi

	while IFS=: read -r src_ dst_ # << +src:dst
	do
		if [ $(echo "$src_" | cut -c1) != + ]
		then
			force_passed=false
		fi

		src_=${src_#+}
		filter_to ! @Refslist "$Hex40 $dst_" "$Refslist"

		if isnonnull "$src_"
		then
			append_to @r_revlist "$src_"
			obj_=$(xfeed "$src_" safe_git_rev_parse)
			append_to @Refslist "$obj_ $dst_"
		fi
	done <<EOF
$1
EOF

	if [ "$force_passed" = false ]
	then
		if [ "$Conf_force_required" = true ]
		then
			echo_die "Implicit force push disallowed by gcrypt configuration."
		else
			echo_info "Due to a longstanding bug, this push implicitly has --force."
			echo_info "Consider explicitly passing --force, and setting"
			echo_info "gcrypt's require-explicit-force-push git config key."
		fi
	fi

	tmp_encrypted="$Tempdir/packP"
	tmp_objlist="$Tempdir/objlP"

	{
		xfeed "$r_revlist" git rev-list --objects --stdin --
		repack_if_needed @r_pack_delete
	} > "$tmp_objlist"

	# Only send pack if we have any objects to send
	if [ -s "$tmp_objlist" ]
	then
		key_=$(genkey "$Packkey_bytes")
		pack_id=$(export GIT_ALTERNATE_OBJECT_DIRECTORIES=$Tempdir;
			pipefail git pack-objects --stdout < "$tmp_objlist" |
			pipefail ENCRYPT "$key_" |
			tee "$tmp_encrypted" | gpg_hash "$Hashtype")

		append_to @Packlist "pack :${Hashtype}:$pack_id $key_"
		if isnonnull "$r_pack_delete"
		then
			append_to @Keeplist "keep :${Hashtype}:$pack_id 1"
		fi
	fi

	# Generate manifest
	echo_info "Encrypting to: $Recipients"
	echo_info "Requesting manifest signature"

	tmp_manifest="$Tempdir/maniP"
	PRIVENCRYPT "$Recipients" > "$tmp_manifest" <<EOF
$Refslist
$Packlist
$Keeplist
repo $Repoid
$Extnlist
EOF

	# Upload pack
	if [ -s "$tmp_objlist" ]
	then
		PUT "$URL" "$pack_id" "$tmp_encrypted"
	fi

	# Upload manifest
	PUT "$URL" "$Manifestfile" "$tmp_manifest"

	rm -f "$tmp_encrypted"
	rm -f "$tmp_objlist"
	rm -f "$tmp_manifest"

	# Delete packs
	if isnonnull "$r_pack_delete"; then
		REMOVE "$URL" "$(xecho "$r_pack_delete" | \
			while IFS=': ' read -r _ _ pack_
			do
				isnonnull "$pack_" || continue
				xecho "$pack_"
			done)"
	fi

	PUT_FINAL "$URL"

	# ok all updates
	while IFS=: read -r src_ dst_ # << +src:dst
	do
		echo_git "ok $dst_"
	done <<EOF
$1
EOF
	
	echo_git 
}

cleanup_tmpfiles()
{
	if isnonnull "${Tempdir%%*."$$"}"; then
		echo_die "Unexpected Tempdir value: $Tempdir"
	fi
	rm -r -f -- "${Tempdir}" >&2
}

setup()
{
	mkdir -p "$Localdir"

	# Set up a subdirectory in /tmp
	temp_key=$(genkey 9 | tr '/' _)
	Tempdir="${TMPDIR:-/tmp}/git-remote-gcrypt-${temp_key}.$$"
	case "${MSYSTEM:-unknown}" in
	MSYS*|MINGW*)
		mkdir "${Tempdir}"
		echo_info "Warning: Not securing tempdir ${Tempdir} because we are on mingw/msys"
		;;
	unknown|*)
		mkdir -m 700 "${Tempdir}"
		;;
	esac

	trap cleanup_tmpfiles EXIT
	trap 'exit 1' 1 2 3 15

	if isurl rclone "$URL"; then
		echo_info "WARNING: rclone support is experimental."
		echo_info "WARNING: Early adoptors only.  Keep backups."
	fi
}

# handle git-remote-helpers protocol
gcrypt_main_loop()
{
	local input_= input_inner= r_args= temp_key=

	NAME=$1  # Remote name
	URL=$2   # Remote URL

	setup

	while read input_
	do
		case "$input_" in
		capabilities)
			do_capabilities
			;;
		list|list\ for-push)
			do_list
			;;
		fetch\ *)
			r_args=${input_##fetch }
			while read input_inner
			do
				case "$input_inner" in
				fetch*)
				r_args= #ignored
				;;
				*)
				break
				;;
				esac
			done
			do_fetch "$r_args"
			;;
		push\ *)
			r_args=${input_##push }
			while read input_inner
			do
				case "$input_inner" in
				push\ *)
					append_to @r_args "${input_inner#push }"
				;;
				*)
					break
				;;
				esac
			done
			do_push "$r_args"
			;;
		?*)
			echo_die "Unknown input!"
			;;
		*)
			CLEAN_FINAL "$URL"
			exit 0
			;;
		esac
	done
}

if [ "x$1" = x--check ]
then
	NAME=dummy-gcrypt-check
	URL=$2
	setup
	ensure_connected
	git remote remove $NAME 2>/dev/null || true
	if iseq "$Did_find_repo" "no"
	then
		exit 100
	fi
else
	gcrypt_main_loop "$@"
fi
